
package com.myshoutui.manager;

import java.io.IOException;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;

import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ImageButton;
import android.widget.Toast;

import com.facebook.android.Facebook;
import com.facebook.android.FacebookError;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.myshout.Client;
import com.myshout.ConsumerSplash;
import com.myshout.LineItem;
import com.myshout.Offer;
import com.myshout.Person;
import com.myshout.Store;
import com.myshout.StoreCredit;
import com.myshout.StoreOrder;
import com.myshout.Thing;
import com.myshout.android.commmon.LocusManager;
import com.myshout.android.commmon.Utils;
import com.myshout.google.GooglePlaces;
import com.myshout.rest.RestClient;
import com.myshout.rest.RestClientException;
import com.myshout.rest.RestResult;
import com.myshout.twitter.MyShoutTwitter;
import com.myshout.yelp.Business;
import com.myshout.yelp.Deals;
import com.myshout.yelp.MyShoutYelp;
import com.myshout.yelp.Options;
import com.myshout.yelp.Yelp;
import com.myshoutui.R;

public class MyShoutDataManager extends Application {

    public static final int MSG_GOT_OFFERS = 100;
    public static final int MSG_GOT_STORE_CREDITS = 200;
    public static final int MSG_GOT_THINGS = 300;
    public static final int MSG_GOT_THING = 302;
    public static final int MSG_LIMIT_STORES = 301;
    public static final int MSG_GOT_STORE = 400;
    public static final int MSG_CREDIT_UPDATED = 500;
    public static final int MSG_CREDIT_UPDATE_ERROR = 501;
    public static final int MSG_LOGOUT_SUCCESSFUL = 502;
    public static final int MSG_GOT_STORES = 600;
    public static final int MSG_SEARCH_STORES = 601;
    public static final int MSG_GOT_REVERSE_GEOCODE = 700;
    public static final int MSG_LOGIN_SUCCESSFUL = 701;
    public static final int MSG_LOGIN_FAILED = 702;
    public static final int MSG_GET_ERROR = 999;
    public static final int MSG_SEARCH_ITEMS = 1001;
    public static final int MSG_SEARCH_FACTUAL = 1002;
    public static final int MSG_SEARCH_YELP = 1003;
    public static final int MSG_GOT_PURCHASE_HISTORY = 1004;
    public static final int MSG_GOT_DASHBOARD = 1005;

    public static final long REFRESH_INTERVAL = 5 * 60 * 1000; // 1 min

    private Person user;
    private Store store;
    private ArrayList<Offer> offers;
    private ArrayList<StoreCredit> credits;
    private ArrayList<Thing> storeCatalog;
    private ArrayList<Store> followingList;
    private Facebook facebook;
    private Activity mainActivity;

    public void createShortcutOnDesktop(Store s) {
        Intent shortcutIntent = new Intent();
        //ROGER : commenting for the 
    //    shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, getIntentShortcut(s.getGoogleId()));
        shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, s.getName());        
        shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getIconAsBitmap(s.getImageUrl()));
        shortcutIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
        this.sendBroadcast(shortcutIntent);
        //finish();
    }

    private Bitmap getIconAsBitmap(String url) {
        try {            
            return BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream());
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    
   /* public Intent getIntentShortcut(String id)
    {       
        Intent i = new Intent();
        i.putExtra("storeShortcut", id);
        i.setClassName("com.myshout.mall", "FindSomething");
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        return i;
    }*/
    
    public Facebook getFacebook() {
        return facebook;
    }

    public void setFacebook(Facebook facebook) {
        this.facebook = facebook;
    }

    public void setOffers(ArrayList<Offer> offers) {
        this.offers = offers;
    }

    public void setCredits(ArrayList<StoreCredit> credits) {
        this.credits = credits;
    }

    private void setStoreCatalog(ArrayList<Thing> storeCatalog) {
        this.storeCatalog = storeCatalog;
    }

    private ImageButton checkoutButton, cartButton;
    private boolean offersBlended;

    private Handler searchResultsChangeCallbackHandler = null;
    private ArrayList<Handler> catalogChangeCallbackHandlers = null;

    public Store getStore() {
        return store;
    }

    public void setStore(Store s) {
        if (store == null) {
            store = s;
            return;
        }

        if (store.getId() != s.getId()) {
            storeCatalog = null;
            offers = null;
            credits = null;
            store = s;
        }
    }

    public synchronized void setCatalogChangeCallbackHandler(Handler handler) {
        if (catalogChangeCallbackHandlers == null)
            catalogChangeCallbackHandlers = new ArrayList<Handler>();
        if (!catalogChangeCallbackHandlers.contains((handler)))
                catalogChangeCallbackHandlers.add(handler);
    }

    public void setSearchResultChangeCallbackHandler(Handler handler) {
        searchResultsChangeCallbackHandler = handler;
    }

    public Thing getThing(long id) {
        if (storeCatalog == null) return null;
        for (Thing t : storeCatalog)
            if (t.getId() != null && t.getId().longValue() == id)
                return t;
        return null;
    }

    public Offer getOffer(long id) {
        if (offers == null) return null;
        for (Offer o : offers)
            if (o.getId() != null && o.getId().longValue() == id)
                return o;
        return null;
    }

    private ArrayList<LineItem> cart = new ArrayList<LineItem>();

    public void addToCart(LineItem t) {
        cart.add(t);
    }

    public void resetCart() {
        cart = new ArrayList<LineItem>();
    }

    public void removeFromCart(LineItem t) {
        if (cart.contains(t))
            cart.remove(t);
    }

    public int getCartSize() {
        return cart.size();
    }

    public int getCartSize(Thing thing) {
        int count = 0;
        for (LineItem li : cart)
            if (li.getThing().longValue() == thing.getId().longValue())
                count++;
        return count;
    }

    public ArrayList<LineItem> getCart() {
        return cart;
    }

    public void setCheckoutButton(ImageButton b) {
        checkoutButton = b;
    }

    public ImageButton getCheckoutButton() {
        return checkoutButton;
    }

    public void setCartButton(ImageButton b) {
        cartButton = b;
    }

    public ImageButton getCartButton() {
        return cartButton;
    }

    private HashMap<Long, Long> followingMap = new HashMap<Long, Long>(); // (thing/store,person)
    private HashMap<String, Long> followingMapGoogleId = new HashMap<String, Long>(); // (thing/store,person)

    public void setFollowing(ArrayList<Store> following, Activity main) {
        mainActivity = main;
        followingList = following;
        for (Store f : following) {
            if (f != null) {
                if (f.getId() != null) followingMap.put(f.getId(), user.getId());
                followingMapGoogleId.put(f.getGoogleId(), user.getId());
            }
        }

        if (mainActivity != null)
            if (!Utils.APP_FALSE.equals(Utils.getPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES))) {
                Utils.setPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES, Client.getGson().toJson(getFollowing()));
            }
    }

    public ArrayList<Store> getFollowing() {
        if (followingList == null)
            followingList = new ArrayList<Store>();
        return followingList;
    }

    public boolean isFollowing(long storeId) {
        return (followingMap.get(storeId) != null);
    }

    public boolean isFollowing(String googleId) {
        return (followingMapGoogleId.get(googleId) != null);
    }

    public void addFollowing(Store add) {
        if (followingList == null) followingList = new ArrayList<Store>();
            followingList.add(add);
            
        if (add.getId() != null) followingMap.put(add.getId().longValue(), user.getId());
        followingMapGoogleId.put(add.getGoogleId(), user.getId());

        if (mainActivity != null)
            if (!Utils.APP_FALSE.equals(Utils.getPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES))) {
                Utils.setPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES, Client.getGson().toJson(getFollowing()));
            }

    }

    public void removeFollowing(Store delete) {
        followingList.remove(delete);
        if (delete.getId() != null) followingMap.remove(delete.getId());
        followingMapGoogleId.remove(delete.getGoogleId());

        if (mainActivity != null)
            if (!Utils.APP_FALSE.equals(Utils.getPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES))) {
                Utils.setPreference(mainActivity, Utils.APP_PREFS_SAVE_FAVES, Client.getGson().toJson(getFollowing()));
            }
    }

    public void setTwitterAccessDetails(String token, String secret) {
        user.setTwitterToken(token);
        user.setTwitterSecret(secret);
        updateUser(user);
    }

    public void resetTwitterAccessDetails() {
        user.setTwitterToken(null);
        user.setTwitterSecret(null);
        updateUser(user);
    }

    private void updateUser(final Person user) {
        // now validate on server
        final Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Gson gson = Client.getGson();
                try {
                    RestClient.putRawData(Client.personResource, gson.toJson(user));
                } catch (Exception e) {
                    Log.d("MyShoutDataManager", e.toString());
                    e.printStackTrace();
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public AccessToken getTwitterAccessToken() {
        if (user.getTwitterToken() != null && user.getTwitterSecret() != null)
            return new AccessToken(user.getTwitterToken(), user.getTwitterSecret());
        else
            return null;
    }

    // TODO: This has to take vicinity into account
    public void refreshStoresInVicinity(final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    RestResult result = RestClient.get(Client.storeResource);
                    Type storeArrayListType = new TypeToken<ArrayList<Store>>() {
                    }.getType();
                    ArrayList<Store> tmpList = Client.getGson().fromJson(result.getDataAsString(), storeArrayListType);
                    message.what = MSG_GOT_STORES;
                    message.obj = tmpList;
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error refreshing Stores " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void getConsumerDashboard(final String searchString, final long consumerId, final double latitude, final double longitude, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    RestResult result = RestClient.get(Client.personSplashResource(searchString, consumerId, latitude, longitude));
                    ConsumerSplash splash = Client.getGson().fromJson(result.getDataAsString(), ConsumerSplash.class);
                   
                    message.what = MSG_GOT_DASHBOARD;
                    message.obj = splash;
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error refreshing Dashboard " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void getPurchaseHistory(final long personId, final long storeId, final int since, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    RestResult result = RestClient.get(Client.userPurchaseHistoryAtStoreResource(personId, storeId, since));
                    Type storeOrderArrayListType = new TypeToken<ArrayList<StoreOrder>>() {
                    }.getType();
                    ArrayList<StoreOrder> tmpList = Client.getGson().fromJson(result.getDataAsString(), storeOrderArrayListType);
                    message.what = MSG_GOT_PURCHASE_HISTORY;
                    message.obj = tmpList;
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error getting Purchase History " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public Intent callSendActivity(Thing t) {
        Store store = getStore();
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("text/plain");

        StringBuffer sb = new StringBuffer();
        if (t == null) {
            sb.append(R.string.thought_you_may_like_ + store.getName()).append(".\n");
            sb.append(R.string.download_myshout_at +  " http://goo.gl/sjxcC ").append("\n");
            sb.append(R.string.and_visit_ + " http://m.go2myshout.com/" + getUserId() + "/" + store.getId()).append("\n");
            sb.append(R.string._regards + " " + getUserRealname());
        } else {
            sb.append(R.string.hey_thought_you_would_like_this_ + t.getName() + " @ " + store.getName()).append("\n");
            sb.append(R.string.download_myshout_at +  " http://goo.gl/sjxcC ").append("\n");
            sb.append(R.string.and_visit_ + " http://m.go2myshout.com/" + getUserId() + "/" + store.getId() + "/" + t.getId());
        }

        intent.putExtra(android.content.Intent.EXTRA_SUBJECT, R.string.check_out_ + store.getName());
        intent.putExtra(android.content.Intent.EXTRA_TEXT, sb.toString());
        return Intent.createChooser(intent, getString(R.string.share_via));
    }

    public void sendAppRequest(Activity a, Thing t) {
        Bundle params = new Bundle();
        if (t == null) {
            params.putString("message", R.string.check_out_ + store.getName() + ": http://m.go2myshout.com/" + getUserId() + "/" + store.getId());
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append(R.string.hey_i_was_shopping_at_ + store.getName() 
                    + R.string._and_ran_into_something_i_thought_you_would_also_like_ + t.getName()
                    + "&ref=" + user.getId());

            sb.append("\n").append(R.string.visit_ + " http://m.go2myshout.com/" + getUserId() + +store.getId() + "/" + t.getId());
            params.putString("message", sb.toString());
        }
        getFacebook().dialog(a, "apprequests", params, new AppRequestsListener());
    }

    public void publishToWall(Activity a, Thing t) {
        Bundle params = new Bundle();
        params.putString("caption", getString(R.string.app_name));
        if (t == null) {
            params.putString("message", R.string.visit_ 
                    + store.getName() + ": http://m.go2myshout.com/" + getUserId() + "/" + store.getId());
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append(R.string.hey_i_was_shopping_at_  
                    + store.getName() + R.string._and_ran_into_something_i_thought_you_would_also_like_ + t.getName()
                    + "&ref=" + user.getId());
            sb.append("\n").append(R.string.visit_ + " http://m.go2myshout.com/" + getUserId() + "/" + store.getId() + "/" + t.getId());

            params.putString("description", sb.toString());
        }

        params.putString("picture", t != null ? t.getImageUrl() : store.getImageUrl());
        params.putString("name", t != null ? t.getName() : store.getName());

        getFacebook().dialog(a, "feed", params, new UpdateStatusListener());
    }

    /*
     * callback for the feed dialog which updates the profile status
     */
    private class UpdateStatusListener extends BaseDialogListener {
        @Override
        public void onComplete(Bundle values) {
            final String postId = values.getString("post_id");
            if (postId != null) {
                Toast toast = Toast.makeText(getApplicationContext(), R.string.posted_message, Toast.LENGTH_SHORT);
                toast.show();
            } else {
                Toast toast = Toast.makeText(getApplicationContext(), R.string.could_not_post_message, Toast.LENGTH_SHORT);
                toast.show();
            }
        }

        @Override
        public void onFacebookError(FacebookError error) {
            Toast.makeText(getApplicationContext(), R.string.facebook_error_ + error.getMessage(), Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onCancel() {
            Toast toast = Toast.makeText(getApplicationContext(), R.string.update_status_cancelled, Toast.LENGTH_SHORT);
            toast.show();
        }
    }

    /*
     * callback for the app requests dialog which sends an app request to user's
     * friends.
     */
    public class AppRequestsListener extends BaseDialogListener {
        @Override
        public void onComplete(Bundle values) {
            Toast toast = Toast.makeText(getApplicationContext(), R.string.app_request_sent, Toast.LENGTH_SHORT);
            toast.show();
        }

        @Override
        public void onFacebookError(FacebookError error) {
            Toast.makeText(getApplicationContext(), R.string.facebook_error_ + error.getMessage(), Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onCancel() {
            Toast toast = Toast.makeText(getApplicationContext(), R.string.app_request_cancelled, Toast.LENGTH_SHORT);
            toast.show();
        }
    }

    public AnalyzedQuery find(final String database, final String searchString, final String address, final Location location) {
        AnalyzedQuery result = AnalyzedQuery.analyze(searchString);
        StringBuffer concatentatedAddressString = new StringBuffer();
        StringBuffer concatentatedSearchString = new StringBuffer();

        if (searchString != null) {
            int nTerms = searchString.split(" ").length;
            int term = 0;
            for (String s : result.getOutputTerms()) {
                concatentatedSearchString.append(s);
                term++;
                if (term < nTerms)
                    concatentatedSearchString.append("+");
            }
        }

        if (address != null) {
            int nTerms = address.split(" ").length;
            int term = 0;
            for (String s : result.getOutputTerms()) {
                concatentatedAddressString.append(s);
                term++;
                if (term < nTerms)
                    concatentatedAddressString.append("+");
            }

        }
        find2(database, concatentatedSearchString.toString(), concatentatedAddressString.toString(), 
            location, searchResultsChangeCallbackHandler);
        /*
        findGoogle(concatentatedSearchString.toString(), concatentatedAddressString.toString(), 
                location, searchResultsChangeCallbackHandler);
        */
        
        return result;
    }

    public void find2(final String database, final String searchString, final String address, final Location location, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            Message message = Message.obtain();

            @Override
            public void run() {
                try {
                    long t1 = System.currentTimeMillis();
                    RestResult result = RestClient.get(Client.searchResourceConsumer(database, searchString, address, 
                            (float) location.getLatitude(),
                            (float) location.getLongitude()));
                    Type storeArrayListType = new TypeToken<ArrayList<Store>>() {}.getType();
                    Log.e("",  "search result "+ result.getDataAsString());
                    message.obj = Client.getGson().fromJson(result.getDataAsString(), storeArrayListType);
                    ArrayList<Store> stores = (ArrayList<Store>) message.obj;
                    long t2 = System.currentTimeMillis();
                    message.what = MSG_SEARCH_YELP;
                } catch (Exception rce) {
                    rce.printStackTrace();
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error in Search " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void findGoogle(final String searchString, final String address, final Location location, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            Message message = Message.obtain();

            @Override
            public void run() {
                try {
                    long t1 = System.currentTimeMillis();
                    GooglePlaces google  = new GooglePlaces();
                    ArrayList<Store> result = (ArrayList<Store>) 
                            google.performSearch(searchString, address,
                                    location.getLatitude(), location.getLongitude());                         
                    message.obj = result;
                    long t2 = System.currentTimeMillis();
                    Log.d("FindGoogle: " + searchString, "" + (t2-t1 + " ms"));
                    message.what = MSG_SEARCH_YELP;
                } catch (Exception rce) {
                    rce.printStackTrace();
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error in Search " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    static {
        try {
        } catch (Exception e) {

        }
    }

    public String md5(String input) {

        String md5 = null;
        if (null == input)
            return null;

        try {

            // Create MessageDigest object for MD5
            MessageDigest digest = MessageDigest.getInstance("MD5");

            // Update input string in message digest
            digest.update(input.getBytes(), 0, input.length());

            // Converts message digest value in base 16 (hex)
            md5 = new BigInteger(1, digest.digest()).toString(16);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return md5;
    }
    
    public void retrieveLocus(final LocusManager locusManager, final Geocoder geocoder, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                message.what = MSG_GOT_REVERSE_GEOCODE;
                Location location = locusManager.getLastLocation();
                Address address = doReverseGeocoding(location, geocoder);
                message.obj = address;
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public Address doReverseGeocoding(Location location, Geocoder geocoder) {
        if (location == null) {
            return null;
        }

        String sName = null;
        try {
            List<Address> addresses = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 8);
            if (addresses != null) {
                for (int i = 0; i < addresses.size(); i++) {
                    Address anAddress = addresses.get(i);
                    String sState = anAddress.getAdminArea();
                    String sCity = anAddress.getLocality();
                    if (sState != null && sCity != null) {
                        double latitude = anAddress.getLatitude();
                        double longitude = anAddress.getLongitude();
                        return anAddress;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return null;
    }

    public void findYelp(final String searchString, final Location location, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            Message message = Message.obtain();

            @Override
            public void run() {
                try {
                    JsonElement jse = null;
                    Yelp yelp = new Yelp(MyShoutYelp.MYSHOUT_YELP_CONSUMER_KEY, MyShoutYelp.MYSHOUT_YELP_CONSUMER_SECRET,
                            MyShoutYelp.MYSHOUT_YELP_TOKEN, MyShoutYelp.MYSHOUT_YELP_TOKEN_SECRET);

                    String response = yelp.search(searchString, location.getLatitude(), location.getLongitude());
                    Gson gson = Client.getGson();
                    jse = new JsonParser().parse(response);
                    System.out.println(jse.toString());
                    JsonArray jsa = jse.getAsJsonObject().getAsJsonArray("businesses");
                    // System.out.println(jsa.size());
                    ArrayList<Store> stores = new ArrayList<Store>();

                    for (int i = 0; i < jsa.size(); i++) {
                        Business b = gson.fromJson(jsa.get(i).toString(), Business.class);
                        System.out.println(jsa.get(i));
                        System.out.println(b.getName());

                        Store s = new Store();
                        s.setId(-1L * i);
                        s.setName(b.getName());
                        s.setPhoneNumber(b.getDisplay_phone());
                        s.setAddress(b.getLocation().toString());
                        s.setUrl(b.getMobile_url() == null ? b.getUrl() : b.getMobile_url());
                        s.setLatitude((float) b.getLocation().getCoordinate().getLatitude());
                        s.setLongitude((float) b.getLocation().getCoordinate().getLongitude());
                        stores.add(s);

                        if (b.getDeals() != null)
                            for (Deals d : b.getDeals()) {
                                System.out.println("\tDeal:" + d.getTitle());
                                for (Options o : d.getOptions()) {
                                    System.out.println("\t" + o.getTitle());
                                }
                            }

                        StringBuffer sb = new StringBuffer();
                        sb.append("Rating: ").append(b.getRating());
                        s.setDescription(sb.toString());
                    }

                    message.what = MSG_SEARCH_FACTUAL;
                    message.obj = stores;
                } catch (NullPointerException npe) {
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    npe.printStackTrace();
                    Log.d(this.toString(), "Error refreshing Stores from Factual " + npe.getMessage());
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                    message.obj = null;
                    Log.d(this.toString(), "Error refreshing Stores from Factual " + rce.getMessage());
                }
                handler.sendMessage(message);
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void setSearchResults(ArrayList<Thing> searchResults) {
        if (searchResultsChangeCallbackHandler != null) {
            Message changeMessage = Message.obtain();
            changeMessage.obj = searchResults;
            changeMessage.what = MSG_LIMIT_STORES;
            searchResultsChangeCallbackHandler.sendMessage(changeMessage);
        }
    }

    private HashMap<String, Long> lastRefreshTimestamp = new HashMap<String, Long>();

    private boolean needsRefresh(String data) {
        Long lastRefresh = lastRefreshTimestamp.get(data);
        if (lastRefresh == null || (System.currentTimeMillis() - lastRefresh > REFRESH_INTERVAL))
            return true;
        return false;
    }

    public void getStore(final long storeId, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            Message message = Message.obtain();

            @Override
            public void run() {
                try {
                    message.what = MSG_GOT_STORE;
                    if (store != null && store.getId().longValue() == storeId)
                        message.obj = store;
                    else {
                        RestResult result = RestClient.get(Client.storeResource + "/" + storeId);
                        Store s = Client.getGson().fromJson(result.getDataAsString(), com.myshout.Store.class);
                        setStore(s);
                        message.obj = s;
                    }
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                } finally {
                    if (handler != null)
                        handler.sendMessage(message);
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void doLogin(final String username, final String password, final Handler handler) {
        final Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Hashtable<String, String> nvPairs = new Hashtable<String, String>();
                nvPairs.put("username", username);
                nvPairs.put("password", password);
                if (store != null)
                    nvPairs.put("store", "" + getStore().getId().longValue());

                RestResult result;
                Message message = Message.obtain();
                try {
                    result = RestClient.post(Client.personLoginResource, nvPairs);
                    message.what = MSG_LOGIN_SUCCESSFUL;
                    // result is the Person object
                    user = Client.getGson().fromJson(result.getDataAsString(), Person.class);

                    RestClient.setCredentials(username, password);

                    result = RestClient.get(Client.personFollowingResource(user.getId().longValue()));
                    Type storeArrayListType = new TypeToken<ArrayList<Store>>() {
                    }.getType();
                    ArrayList<Store> tmpList = Client.getGson().fromJson(result.getDataAsString(), storeArrayListType);
                    setFollowing(tmpList, null);
                    user.setUsername(username);
                    user.setPassword(password);
                    message.obj = user;
                } catch (Exception e) {
                    message.what = MSG_LOGIN_FAILED;
                    message.obj = null;
                } finally {
                    handler.sendMessage(message);
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void getStoreForUser(final String username, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            Message message = Message.obtain();

            @Override
            public void run() {
                try {
                    message.what = MSG_GOT_STORE;
                    if (store != null)
                        message.obj = store;
                    else {
                        RestResult result = RestClient.get(Client.personsStoreResource(user.getId()));
                        store = Client.getGson().fromJson(result.getDataAsString(), com.myshout.Store.class);
                        message.obj = store;
                    }
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                } finally {
                    if (handler != null)
                        handler.sendMessage(message);
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void logout(final Handler handler) {
        final Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                // TODO: Doing this when not logged pops an NPE
                Hashtable<String, String> nvPairs = new Hashtable<String, String>();
                nvPairs.put("username", user.getUsername());
                nvPairs.put("password", user.getPassword());

                RestResult result;
                try {
                    result = RestClient.post(Client.personLogoutResource, nvPairs);
                    Message message = Message.obtain();
                    message.what = MSG_LOGOUT_SUCCESSFUL;
                    handler.sendMessage(message);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public String getUsername() {
        return user.getUsername();
    }

    public String getUserRealname() {
        return user.getFirstname() + " " + user.getLastname();
    }

    public boolean isLoggedIn() {
        return user != null;
    }

    public synchronized void setUser(Person user) {
        this.user = user;

        // reset internal state
        storeCatalog = null;
        store = null;
        offers = null;
        credits = null;
        catalogChangeCallbackHandlers = null;
        lastRefreshTimestamp = new HashMap<String, Long>();
    }

    public Long getUserId() {
        return (user != null ? user.getId() : 0L);
    }

    public String getUserPhone() {
        return user.getPhoneNumber();
    }

    public ArrayList<Offer> getOffers() {
        return offers;
    }

    public ArrayList<StoreCredit> getCredits() {
        return credits;
    }

    public ArrayList<Thing> getStoreCatalog() {
        return storeCatalog;
    }

    public void postToTwitter(String tweet) throws TwitterException {
        Twitter twitter = new TwitterFactory().getInstance();
        AccessToken token = getTwitterAccessToken();
        twitter.setOAuthConsumer(MyShoutTwitter.consumerKey, MyShoutTwitter.consumerSecret);
        twitter.setOAuthAccessToken(token);
        twitter.updateStatus(tweet);
    }

    public void synchronizeStoreCreditsAndOffers(final Handler handler, final ArrayList<LineItem> cart) {
        // TODO: Have to handle used up 1-time offers
        // TODO: Handle Store Credits purchased
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {

                // Apply used up Store Credits to Server
                ArrayList<StoreCredit> tmpList = new ArrayList<StoreCredit>();
                tmpList.addAll(credits);
                Message message = Message.obtain();
                message.what = MSG_CREDIT_UPDATED;
                for (StoreCredit sc : tmpList) {
                    try {
                        Gson gson = Client.getGson();
                        RestClient.putRawData(Client.storeCreditResource, gson.toJson(sc));
                        credits.remove(sc);
                    } catch (RestClientException e) {
                        message.what = MSG_CREDIT_UPDATE_ERROR;
                    }
                }
                message.obj = credits; // if all updates succeeded this would be
                                       // empty list
                lastRefreshTimestamp.remove("credits"); // force refresh
                credits = null;

                Gson gson = Client.getGson();

                // Apply recently purchased Store Credits
                for (LineItem li : cart) {
                    Thing ci = getThing(li.getThing().longValue());
                    // Buy Store Credit
                    Offer offer = ci.getOffer();
                    if (offer != null) {
                        StoreCredit storeCredit = null;
                        if (offer.getType() == Offer.STORE_CREDIT_CASH) {
                            storeCredit = new StoreCredit();
                            storeCredit.setStore(offer.getStore());
                            storeCredit.setType(offer.getType());
                            storeCredit.setUser(user.getId());

                            storeCredit.setThing(0L);
                            storeCredit.setInitUnits(0);
                            storeCredit.setUsedUnits(0);

                            storeCredit.setInitValue(offer.getPaidValue());
                            storeCredit.setRemainingValue(offer.getFaceValue());
                        } else if (offer.getType() == Offer.STORE_CREDIT_UNITS) {
                            storeCredit = new StoreCredit();
                            storeCredit.setStore(offer.getStore());
                            storeCredit.setType(offer.getType());
                            storeCredit.setThing(offer.getThing());
                            storeCredit.setUser(user.getId());

                            storeCredit.setInitUnits(offer.getUnits());
                            storeCredit.setUsedUnits(0);

                            storeCredit.setInitValue(offer.getPaidValue());
                            storeCredit.setRemainingValue(offer.getFaceValue());

                        } else
                            continue;
                        if (storeCredit != null) {

                            storeCredit.setPurchaseTime(new Date());
                            Calendar startTime = Calendar.getInstance();
                            startTime.add(Calendar.HOUR, 7 * 24 * 30); // TODO:
                                                                       // HACK 7
                                                                       // Months!!
                            storeCredit.setExpiryTime(startTime.getTime());

                            try {
                                RestClient.postRawData(Client.storeCreditResource, gson.toJson(storeCredit));
                            } catch (RestClientException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }
                }

                if (handler != null)
                    handler.sendMessage(message);
            };
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void refreshStoreCredits(final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    ArrayList<StoreCredit> creditsList = getCredits();
                    if (creditsList != null && !needsRefresh("credits")) {
                        message.obj = creditsList;
                        message.what = MSG_GOT_STORE_CREDITS;
                    } else {
                        RestResult result = RestClient.get(Client.storeCreditsForCustomerResource(user.getId(), store.getId()));
                        Type storeCreditArrayListType = new TypeToken<ArrayList<StoreCredit>>() {
                        }.getType();
                        ArrayList<StoreCredit> list = Client.getGson().fromJson(result.getDataAsString(), storeCreditArrayListType);
                        setCredits(list);
                        lastRefreshTimestamp.put("credits", System.currentTimeMillis());
                        message.obj = list;
                        message.what = MSG_GOT_STORE_CREDITS;
                    }
                } catch (RestClientException rce) {
                    Log.d(this.toString(), "Error getting store credit " + rce.getMessage());
                    message.what = MSG_GET_ERROR;
                } finally {
                    if (handler != null)
                        handler.sendMessage(message);
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void refreshOffers(final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    ArrayList<Offer> offersList = getOffers();
                    if (offersList != null && !needsRefresh("offers")) {
                        message.obj = offersList;
                    } else {
                        RestResult result = RestClient.get(Client.allOffersForPersonAtStoreResource(user.getId(), store.getId()));
                        Type offerArrayListType = new TypeToken<ArrayList<Offer>>() {
                        }.getType();
                        ArrayList<Offer> list = Client.getGson().fromJson(result.getDataAsString(), offerArrayListType);
                        lastRefreshTimestamp.put("offers", System.currentTimeMillis());
                        message.obj = list;
                        setOffers(list);
                        if (!offersBlended) {
                            blendOfferInCatalog();
                        }
                    }
                    message.what = MSG_GOT_OFFERS;
                } catch (Exception rce) {
                    message.what = MSG_GET_ERROR;
                    Log.d(this.toString(), "Error getting offers " + rce.getMessage());
                } finally {
                    if (!offersBlended) {
                        blendOfferInCatalog();
                    }
                    if (handler != null)
                        handler.sendMessage(message);
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    private void blendOfferInCatalog() {
        boolean changed = false;
        if (offers == null) return;
        for (Offer o : offers) {
            if (o.getThing() != null) {
                if (o.getThing() == null) continue;
                Thing t = getThing(o.getThing().longValue());
                if (t == null) continue;
                Thing ot = t.copy();
                ot.setQuantity(t.getQuantity());
                ot.setUnits(t.getUnits());
                ot.setCount(o.getUnits()); // TODO: HACK need in_stock, count,
                                           // quantity, measure in Thing
                ot.setPrice(o.getPaidValue());
                ot.setOffer(o);
                storeCatalog.add(ot);
                changed = true;
            }
        }
        if (changed) {
            if (catalogChangeCallbackHandlers != null) {
                for (Handler h : catalogChangeCallbackHandlers) {
                    Message changeMessage = Message.obtain();
                    changeMessage.obj = storeCatalog;
                    changeMessage.what = MSG_GOT_THINGS;
                    h.sendMessage(changeMessage);
                }
            }
            lastRefreshTimestamp.put("things", System.currentTimeMillis());
        }
        offersBlended = true;
    }

    public void refreshThings(final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    ArrayList<Thing> thingsList = getStoreCatalog();
                    if (thingsList != null && !needsRefresh("things")) {
                        message.obj = thingsList;
                    } else {
                        RestResult result = RestClient.get(Client.thingsInStoreResource(store.getId()));
                        Type thingListType = new TypeToken<ArrayList<Thing>>() {}.getType();
                        ArrayList<Thing> tmpList = Client.getGson().fromJson(result.getDataAsString(), thingListType);
                        message.obj = tmpList;
                        lastRefreshTimestamp.put("things", System.currentTimeMillis());
                        setStoreCatalog(tmpList);
                        offersBlended = false;
                    }
                    message.what = MSG_GOT_THINGS;
                } catch (Exception rce) {
                    rce.printStackTrace();
                    Log.d(this.toString(), "Error refreshing Items " + rce.getMessage());
                    message.what = MSG_GET_ERROR;
                } finally {
                    if (handler != null && message.what != MSG_GET_ERROR) {
                        handler.sendMessage(message);
                        if (catalogChangeCallbackHandlers != null) {
                            for (Handler h : catalogChangeCallbackHandlers) {
                                Message newMessage = Message.obtain();
                                newMessage.what = message.what;
                                newMessage.obj = message.obj;
                                h.sendMessage(newMessage);
                            }
                        }
                    }
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }

    public void getThing(final long thingId, final Handler handler) {
        Runnable bgRunner = new Runnable() {
            @Override
            public void run() {
                Message message = Message.obtain();
                try {
                    ArrayList<Thing> thingsList = getStoreCatalog();
                    if (thingsList != null) {
                        for (Thing t : thingsList) {
                            if (t.getId().longValue() == thingId) {
                                message.obj = t;
                                break;
                            }
                        }
                    } else {
                        RestResult result = RestClient.get(Client.thingResource(thingId));
                        Thing t = Client.getGson().fromJson(result.getDataAsString(), Thing.class);
                        message.obj = t;
                    }
                    message.what = MSG_GOT_THING;
                } catch (Exception rce) {
                    Log.d(this.toString(), "Error getting Item: " + rce.getMessage());
                    message.what = MSG_GET_ERROR;
                } finally {
                    if (handler != null && message.what != MSG_GET_ERROR) {
                        handler.sendMessage(message);
                    }
                }
            }
        };
        Thread backgroundThread = new Thread(bgRunner);
        backgroundThread.start();
    }
}
